/*
 * Copyright 2010-2020 Alfresco Software, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.activiti.core.el.juel.tree;

import jakarta.el.ELException;
import jakarta.el.ValueExpression;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.activiti.core.el.juel.misc.TypeConverter;

/**
 * Bindings, usually created by a {@link Tree}.
 *
 * @author Christoph Beck
 */
public class Bindings implements TypeConverter {

    private static final long serialVersionUID = 1L;

    private static final Method[] NO_FUNCTIONS = new Method[0];
    private static final ValueExpression[] NO_VARIABLES = new ValueExpression[0];

    /**
     * Wrap a {@link Method} for serialization.
     */
    private static class MethodWrapper implements Serializable {

        private static final long serialVersionUID = 1L;

        private transient Method method;

        private MethodWrapper(Method method) {
            this.method = method;
        }

        private void writeObject(ObjectOutputStream out)
            throws IOException, ClassNotFoundException {
            out.defaultWriteObject();
            out.writeObject(method.getDeclaringClass());
            out.writeObject(method.getName());
            out.writeObject(method.getParameterTypes());
        }

        private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            Class<?> type = (Class<?>) in.readObject();
            String name = (String) in.readObject();
            Class<?>[] args = (Class<?>[]) in.readObject();
            try {
                method = type.getDeclaredMethod(name, args);
            } catch (NoSuchMethodException e) {
                throw new IOException(e.getMessage());
            }
        }
    }

    private transient Method[] functions;
    private final ValueExpression[] variables;
    private final TypeConverter converter;

    /**
     * Constructor.
     */
    public Bindings(Method[] functions, ValueExpression[] variables) {
        this(functions, variables, TypeConverter.DEFAULT);
    }

    /**
     * Constructor.
     */
    public Bindings(
        Method[] functions,
        ValueExpression[] variables,
        TypeConverter converter
    ) {
        super();
        this.functions =
            functions == null || functions.length == 0
                ? NO_FUNCTIONS
                : functions;
        this.variables =
            variables == null || variables.length == 0
                ? NO_VARIABLES
                : variables;
        this.converter = converter == null ? TypeConverter.DEFAULT : converter;
    }

    /**
     * Get function by index.
     * @param index function index
     * @return method
     */
    public Method getFunction(int index) {
        return functions[index];
    }

    /**
     * Test if given index is bound to a function.
     * This method performs an index check.
     * @param index identifier index
     * @return <code>true</code> if the given index is bound to a function
     */
    public boolean isFunctionBound(int index) {
        return index >= 0 && index < functions.length;
    }

    /**
     * Get variable by index.
     * @param index identifier index
     * @return value expression
     */
    public ValueExpression getVariable(int index) {
        return variables[index];
    }

    /**
     * Test if given index is bound to a variable.
     * This method performs an index check.
     * @param index identifier index
     * @return <code>true</code> if the given index is bound to a variable
     */
    public boolean isVariableBound(int index) {
        return (
            index >= 0 && index < variables.length && variables[index] != null
        );
    }

    /**
     * Apply type conversion.
     * @param value value to convert
     * @param type target type
     * @return converted value
     * @throws ELException
     */
    public <T> T convert(Object value, Class<T> type) {
        return converter.convert(value, type);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Bindings) {
            Bindings other = (Bindings) obj;
            return (
                Arrays.equals(functions, other.functions) &&
                Arrays.equals(variables, other.variables) &&
                converter.equals(other.converter)
            );
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (
            Arrays.hashCode(functions) ^
            Arrays.hashCode(variables) ^
            converter.hashCode()
        );
    }

    private void writeObject(ObjectOutputStream out)
        throws IOException, ClassNotFoundException {
        out.defaultWriteObject();
        MethodWrapper[] wrappers = new MethodWrapper[functions.length];
        for (int i = 0; i < wrappers.length; i++) {
            wrappers[i] = new MethodWrapper(functions[i]);
        }
        out.writeObject(wrappers);
    }

    private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        MethodWrapper[] wrappers = (MethodWrapper[]) in.readObject();
        if (wrappers.length == 0) {
            functions = NO_FUNCTIONS;
        } else {
            functions = new Method[wrappers.length];
            for (int i = 0; i < functions.length; i++) {
                functions[i] = wrappers[i].method;
            }
        }
    }
}
